// Author: Bjorn Schobben

#include "Slice.h"
#include <assert.h>

// A slice has a screenheight, and keeps this height until it is destroyed
Slice::Slice(unsigned int nScreenHeight): perspectiveBuffer(0), sliceBuffer(0), screenHeight(nScreenHeight), scalarScreenHeight(nScreenHeight)
{
	// Allocate slice buffer, a full screenheight slice of pixels
	sliceBuffer = new Pixel[screenHeight];

	// Initialize perspective buffer (only needs to be calculated once)
	InitializePerspectiveBuffer();
}

Slice::Slice(const Slice& rhs): perspectiveBuffer(0), scalarScreenHeight(rhs.scalarScreenHeight),	screenHeight(rhs.screenHeight), sliceBuffer(0)
{
	sliceBuffer = new Pixel[screenHeight];

	// Copy pixels from rhs slice
	for(unsigned int i=0; i<screenHeight; i++)
		sliceBuffer[i] = rhs.sliceBuffer[i];

	// Initialize perspective buffer (only needs to be calculated once)
	InitializePerspectiveBuffer();
}

Slice::~Slice()
{
	if (0 != sliceBuffer)
	{
		delete[] sliceBuffer;
	}

	if (0 != perspectiveBuffer)
	{
		delete[] perspectiveBuffer;
	}
}

void Slice::InitializePerspectiveBuffer()
{
	// Only need one half of the screen for the perspective-ish buffer
	int perspectiveHeight = (screenHeight+1)/2;
	Scalar perspectiveDistance;

	// Allocate the perspective buffer
	perspectiveBuffer = new Scalar[perspectiveHeight];

	// And now fill it with perspective gradients
	// This fills the buffer with values between 1 and (almost) infinity
	for(int y=0; y < perspectiveHeight; y++)
	{
		perspectiveDistance = 0;

		// Make sure we don't divide by zero
		if (y*2 != scalarScreenHeight.GetInt())
			perspectiveDistance = scalarScreenHeight / ( (Scalar)(y*2) - scalarScreenHeight);

		perspectiveBuffer[y] = perspectiveDistance;
	}
}


void Slice::Initialize(const Ray& ray, const Vector& nEyePoint, const World& world)
{
	// Cache wall intersection and distance 
	wallIntersection = ray.GetIntersection();
	wallDistance = ray.GetDistance();

	// Cache eyepoint
	eyepoint = nEyePoint;

	// Calculate (half) projected wall height
	scalarWallHeight = ( scalarScreenHeight / wallDistance ) / 2;
	wallHeight = scalarWallHeight.GetRoundInt() + 1;


	// Calculate where slice starts and ends on screen
	sliceStart = (screenHeight/2) - wallHeight;
	sliceEnd = (screenHeight/2) + wallHeight;

	// Clip slice to screen dimensions
	clippedSliceStart = Clip(sliceStart);
	clippedSliceEnd = Clip(sliceEnd);

	// Get default world textures
	SetupTextures(ray, world);

	// Precalculate textureX (textureY is calculated per y)
	textureX = CalculateTextureX(ray.GetSideHit(), wallIntersection, *wallTexture);

	// And render the slice to the buffer
	RenderSlice(world);
}

void Slice::SetupTextures(const Ray& ray, const World& world)
{
	Cell::Side wallSide = HitSideToCellSide(ray.GetSideHit());
	unsigned int wallTextureIndex = world.GetTextureCell(ray.GetMapIntersectionX(), ray.GetMapIntersectionY(), wallSide);
	unsigned int ceilingTextureIndex = world.GetCeilingTextureIndex();
	unsigned int floorTextureIndex = world.GetFloorTextureIndex();

	if (-1 == wallTextureIndex)
		wallTextureIndex = world.GetDefaultTextureIndex();

	wallTexture = &world.GetTexture(wallTextureIndex);
	ceilingTexture = &world.GetTexture(ceilingTextureIndex);
	floorTexture = &world.GetTexture(floorTextureIndex);
}

void Slice::RenderSlice(const World& world)
{
	Scalar currentFloorDistance, t, worldXs, worldYs, tinv;

	// Render floor and ceiling slice

	// The floor has always 1 more or equal amount of pixels to draw than the ceiling because of the divide by 2,
	// so we use the floor height as loop
	for(int y=screenHeight-1; y >= clippedSliceEnd; y--)
	{
		// ymirror is the ceiling counterpart of the floor y
		int ymirror = screenHeight - y - 1;

		// Get the (precalculated) perspective gradient (ymirror ranges from 0 to just below the screenHeight/2),
		// and divide by the wall distance to get our parameter for the parametric line
		// Note that if wallDistance is almost 0, the wall height would be bigger than the screen height, and this loop won't be entered
		t = (perspectiveBuffer[ymirror] / wallDistance).Abs();
		tinv = (Scalar)1 - t;

		// Calculate world x and y coordinates
		worldXs = t * wallIntersection.GetX() + tinv * eyepoint.GetX();
		worldYs = t * wallIntersection.GetY() + tinv * eyepoint.GetY();

		unsigned int mapX = worldXs.GetInt()/1;
		Scalar mapExtX = mapX*1;
		unsigned int mapY = worldYs.GetInt()/1;
		Scalar mapExtY = mapY*1;		


		// Initialize current ceiling/floor textures with default textures
		const Texture* currentCeilingTexture = ceilingTexture;
		const Texture* currentFloorTexture = floorTexture;

		unsigned int cellCeilingIndex = world.GetTextureCell(mapX, mapY, Cell::CEILING);
		unsigned int cellFloorIndex = world.GetTextureCell(mapX, mapY, Cell::FLOOR);

		// Cell specific textures?
		if (-1 != cellCeilingIndex)
		{
			currentCeilingTexture = &world.GetTexture(cellCeilingIndex);
		}

		if (-1 != cellFloorIndex)
		{
			currentFloorTexture = &world.GetTexture(cellFloorIndex);
		}


		// Calculate ceiling and floor texture coordinates
		unsigned int ceilingTexX = ((worldXs - mapExtX)*currentCeilingTexture->GetWidth()).GetInt();
		unsigned int ceilingTexY = ((worldYs - mapExtY)*currentCeilingTexture->GetHeight()).GetInt();
		unsigned int floorTexX = ((worldXs - mapExtX)*currentFloorTexture->GetWidth()).GetInt();
		unsigned int floorTexY = ((worldYs - mapExtY)*currentFloorTexture->GetHeight()).GetInt();


		// Put floor and ceiling pixel in the buffer
		sliceBuffer[ymirror] = currentCeilingTexture->GetPixel(ceilingTexX, ceilingTexY);
		sliceBuffer[y] = currentFloorTexture->GetPixel(floorTexX, floorTexY);
	}


	// Render wall slice
	for(int y=clippedSliceStart; y < clippedSliceEnd; y++)
	{
		// Calculate textureY for the wall
		textureY = (y-sliceStart)*wallTexture->GetHeight()/(sliceEnd-sliceStart);

		// And put the wall pixel in the buffer
		sliceBuffer[y] = wallTexture->GetPixel(textureX, textureY);
	}
}


const Pixel& Slice::GetPixel(unsigned int y) const
{
	return sliceBuffer[y];
}

// Clip y to the screen coordinate
int Slice::Clip(int y)
{
	if (y < 0)
		y = 0;

	if (y > screenHeight)
		y = screenHeight;

	return y;
}

unsigned int Slice::CalculateTextureX(Ray::SideHit sideHit, const Vector& intersect, const Texture& texture)
{
	Scalar x = intersect.GetX();
	Scalar y = intersect.GetY();
	unsigned int mapX = x.GetInt();
	Scalar mapExtX = mapX;
	unsigned int mapY = y.GetInt();
	Scalar mapExtY = mapY;

	// Eyepoint to wall sidehit sketch:
	//                 o
	//                  \
	//                   \ SOUTH
	//                   _\___
	//                  |     |
	//            WEST /|     |\ EAST
	//                / |_____| \
	//               /    NORTH  \
	//              /      \      \
	//             /        \      o
	//            o          \ 
	//                        \
	//                         o

	// NORTH = FRONT
	// EAST = RIGHT
	// SOUTH = BACK
	// WEST = LEFT



	// Calculate textureX based on which side the wall was hit
	switch(sideHit)
	{

		case Ray::SOUTH:
		{
			Scalar scalarTexSpace = (x-mapExtX) * texture.GetWidth();

			return texture.GetWidth()-1 - scalarTexSpace.GetInt();
		}

		case Ray::EAST:
		{
			Scalar scalarTexSpace = (y-mapExtY) * texture.GetWidth();

			return texture.GetWidth()-1 - scalarTexSpace.GetInt();
		}

		case Ray::NORTH:
		{
			Scalar scalarTexSpace = (x-mapExtX) * texture.GetWidth();

			return scalarTexSpace.GetInt();
		}

		case Ray::WEST:
		{
			Scalar scalarTexSpace = (y-mapExtY) * texture.GetWidth();

			return scalarTexSpace.GetInt();
		}

		default:
			return 0;
	}
}

Cell::Side Slice::HitSideToCellSide(Ray::SideHit sideHit)
{
	// See sketch
	switch(sideHit)
	{
		case Ray::SOUTH:
			return Cell::BACK;

		case Ray::EAST:
			return Cell::RIGHT;

		case Ray::NORTH:
			return Cell::FRONT;

		case Ray::WEST:
			return Cell::LEFT;

		default:
			return Cell::FRONT;
	}
}
